package org.hong.monkey.network.util;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class JavaUtils {
    private static final Logger logger = LoggerFactory.getLogger(JavaUtils.class);

    public static final long DEFAULT_DRIVER_MEM_MB = 1024;

    public static void closeQuietly(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException e) {
            logger.error("IOException should not have been thrown.", e);
        }
    }

    public static int nonNegativeHash(Object obj) {
        if (obj == null) { return 0; }
        int hash = obj.hashCode();
        return hash != Integer.MIN_VALUE ? Math.abs(hash) : 0;
    }


    public static ByteBuffer stringToBytes(String s) {
        return Unpooled.wrappedBuffer(s.getBytes(Charsets.UTF_8)).nioBuffer();
    }

    public static String bytesToString(ByteBuffer b) {
        return Unpooled.wrappedBuffer(b).toString(Charsets.UTF_8);
    }

    public static void deleteRecursively(File file) throws IOException {
        if (file == null) return;

        if (file.isDirectory() && !isSymlink(file)) {
            IOException savedIOException = null;
            for (File child : listFilesSafely(file)) {
                try {
                    deleteRecursively(child);
                } catch (IOException e) {
                    savedIOException = e;
                }
            }

            if (savedIOException != null)
                throw savedIOException;
        }

        boolean deleted = file.delete();
        if (deleted && file.exists())
            throw new IOException("Failed to delete: " + file.getAbsolutePath());
    }

    private static File[] listFilesSafely(File dir) throws IOException {
        if (dir.exists()) {
            File[] files = dir.listFiles();
            if (files == null)
                throw new IOException("Failed to list files for dir: " + dir);
            return files;
        } else
            return new File[0];
    }

    private static boolean isSymlink(File file) throws IOException {
        Preconditions.checkNotNull(file);
        File fileInCanonicalDir = null;
        if (file.getParent() == null)
            fileInCanonicalDir = file;
        else
            fileInCanonicalDir = new File(file.getParentFile().getCanonicalFile(), file.getName());
        return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
    }

    private static final ImmutableMap<String, TimeUnit> timeSuffixes =
            ImmutableMap.<String, TimeUnit>builder()
            .put("us", TimeUnit.MICROSECONDS)
            .put("ms", TimeUnit.MILLISECONDS)
            .put("s", TimeUnit.SECONDS)
            .put("m", TimeUnit.MINUTES)
            .put("min", TimeUnit.MINUTES)
            .put("h", TimeUnit.HOURS)
            .put("d", TimeUnit.DAYS)
            .build();

    private static final ImmutableMap<String, ByteUnit> byteSuffixes =
            ImmutableMap.<String, ByteUnit>builder()
            .put("b", ByteUnit.BYTE)
            .put("k", ByteUnit.KiB)
            .put("kb", ByteUnit.KiB)
            .put("m", ByteUnit.MiB)
            .put("mb", ByteUnit.MiB)
            .put("g", ByteUnit.GiB)
            .put("gb", ByteUnit.GiB)
            .put("t", ByteUnit.TiB)
            .put("tb", ByteUnit.TiB)
            .put("p", ByteUnit.PiB)
            .put("pb", ByteUnit.PiB)
            .build();

    private static long parseTimeString(String str, TimeUnit unit) {
        String lower = str.toLowerCase().trim();
        try {
            Matcher m = Pattern.compile("(-?[0-9]+)([a-z]+)?").matcher(lower);
            if (!m.matches())
                throw new NumberFormatException("Failed to prase time string: " + str);

            long val = Long.parseLong(m.group(1));
            String suffix = m.group(2);
            if (suffix != null && !timeSuffixes.containsKey(suffix))
                throw new NumberFormatException("Invalid suffix: \"" + suffix + "\"");

            return unit.convert(val, suffix != null ? timeSuffixes.get(suffix) : unit);
        } catch (NumberFormatException e) {
            String timeError = "Time must be specified as [s | ms | us | m | min | h | d]";
            throw new NumberFormatException(timeError + "\n" + e.getMessage());
        }
    }

    public static long timeStringAsMs(String str) {
        return parseTimeString(str, TimeUnit.MILLISECONDS);
    }

    public static long timeStringAsSec(String str) {
        return parseTimeString(str, TimeUnit.SECONDS);
    }

    private static long parseByteString(String str, ByteUnit unit) {
        String lower = str.toLowerCase().trim();

        try {
            Matcher m = Pattern.compile("(-?[0-9]+)([a-z]+)?").matcher(lower);
            Matcher fractionMatcher = Pattern.compile("([0-9]+\\.[0-9]+)([a-z]+)?").matcher(lower);

            if (m.matches()) {
                long val = Long.parseLong(m.group(1));
                String suffix = m.group(2);

                if (suffix != null && !byteSuffixes.containsKey(suffix))
                    throw new NumberFormatException("Invalid suffix: \"" + suffix + "\"");

                return unit.convertFrom(val, suffix != null ? byteSuffixes.get(suffix) : unit);
            } else if (fractionMatcher.matches())
                throw new NumberFormatException("Fractional values ar not supported. Input was:" + fractionMatcher.group(1));
            else
                throw new NumberFormatException("Failed to parse byte string: " + str);
        } catch (NumberFormatException e) {
            String timeError = "Size must be specified as [b | k | m | g | t | p]";
            throw new NumberFormatException(timeError + "\n" + e.getMessage());
        }
    }

    public static long byteStringAsBytes(String str) {
        return parseByteString(str, ByteUnit.BYTE);
    }

    public static long byteStringAsKb(String str) {
        return parseByteString(str, ByteUnit.KiB);
    }

    public static long byteStringAsMb(String str) {
        return parseByteString(str, ByteUnit.MiB);
    }

    public static long byteStringAsGb(String str) {
        return parseByteString(str, ByteUnit.GiB);
    }

    public static byte[] bufferToArray(ByteBuffer buffer) {
        if (buffer.hasArray() && buffer.arrayOffset() == 0
                && buffer.array().length == buffer.remaining())
        {
            return buffer.array();
        } else {
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            return bytes;
        }
    }
}
